Trotzdem will Stefan Stuntz noch mehr von uns. :-) Wir sollen in den Programmen das Konzept der Custom Classes nutzen. Indem wir die Funktion unserer Software in objektorientierte Dingsdas (der Profi sagt nicht Dingsda, sondern "Objekt") quetschen, erhält die MUI-GUI noch ihren besonderen Kick. Für uns wird dabei so einiges übersichtlicher, kann aber auch daneben gehen, je nachdem, wie man sich halt anstellt. Wie immer im Leben.
Was soll das "Private" in der Überschrift? Private Klassen befinden sich direkt im Quelltext des Programms, öffentliche Klassen sind als Library verfügbar (#?.mcc) und haben evtl. einen Prefs-Editor (#?.mcp) dabei. Sie können von mehreren Applikationen benutzt werden.
Ich will mich erstmal an private Klassen wagen, Public Classes habe ich selbst noch nie programmiert.
Jetzt ist alles zentral gesteuert. Die GUI-Elemente selbst empfangen Anweisungen ("Disable Dich! ähm: Disabel dich!") und geben Ereignisse weiter. Das Fenster, das man geöffnet hat, fungiert nur als Rahmen für die Objekte, die es beinhaltet. Alle Intelligenz geht vom Hauptprogramm aus, möglichst noch alles in die Hauptschleife gequetscht, in der die Notifies abgefragt werden.
Bei größeren Projekten mit mehreren Fenstern wird die Verwaltung der Ereignisse in einer Hauptschleife nicht nur unübersichtlich, teilweise auch unmöglich.
Man stelle sich ein Programm vor, das seine Funktionsfenster mehrmals gleichzeitig öffnen kann. Zum Beispiel die Lister oder Knopfbänke von DirOpus. Alle einzelnen Lister (die Instanzen des Lister-Objekts) in der gleichen Schleife abzufragen, wäre nur möglich, wenn man sich Ekeligkeiten zur Definition der "Ereignisnummern" überlegt, die mit MUIM_Application_ReturnID an die Hauptschleife übergeben werden.
Wesentlich eleganter wäre es, wenn die Objekte sich autark verwalten würden. Wenn jeder Lister eine Abfrage für sich hätte, in der er sein Listview und die Buttons kontrolliert.
Dafür erzeugt man eine Sub-Klasse, eine Ableitung der Klasse "Window" von MUI. Die eigene Window - Custom Class kann alles, was das MUI-Window auch kann. Es kann aber beim Initialisieren gleich die Gadgets anlegen und bei Notifies sich selbst aufrufen. Es empfängt dann die Benutzereingaben, die in ihm passieren, und kann seine GUI-Elemente selbst modifizieren und Aktionen im eigentlichen Programm auslösen.
Dazu ist es nötig, daß wir Funktionen in C definieren, die nicht das C-Programm selbst direkt aufruft, sondern die von MUI indirekt aufgerufen werden. Wir übergeben MUI einen Zeiger auf unsere Funktion, das ist in vielen Hochsprachen möglich. Näheres dazu später. Ein halbwegs brauchbarer (aber wegen der Länge verschriener) Beispielquelltext für MUI ist der psi.c, der "Public Screen Investigator" hat ja auch Editorfenster, die sich mehrmals öffnen lassen. Ich werde darauf in einem folgenden Kursteil noch zurückkommen.
Natürlich kann man nicht nur von "Window" Custom Classes erzeugen. Alle anderen Objekte sind auch möglich. Zum Beispiel ein Textfeld, das den anzuzeigenden Text nicht aus dem restlichen Programm bekommt, sondern selbständig den freien Speicher anzeigt, oder ähnliches. Oder ein Slider, der ein Längenmaß einstellt, und dabei selbständig zwischen Zentimetern, Zoll und Lichtsekunden umrechnet. Oder eine Gruppe, die ihren Inhalt selbständig anhand einer HTML-Datei erstellt. :-)
Grenzen sind eigentlich nicht gesetzt. Eine hübsche Anwendung ist auch Drag&Drop. Aus dem normalen Farbeinsteller-Popup machen wir ein Popup, das sich ziehen und auf ein anderes Popup fallenlassen läßt. Letzteres liest die Farbwerte des gezogenen Popups aus und trägt sie bei sich ein. Der MUI-Einsteller macht das auch für Rahmen und Oberflächen vor.
Die MUI-Autodocs verwenden sinnvollerweise Begriffe der objektorientierten Programmierung. Ohne da jetzt allzu tief in Definitions- und Beispielorgien ("Lampe (mit Stromkabeln), Stehlampe (schließt Lampe ein, zusätzlich mit Schalter)...") zu verfallen, will ich schnell die wichtigsten Begriffe klarstellen:
Eine Klasse ist ein abstraktes Dings, das Daten und Methoden, also klassische Funktionen beinhaltet.
Eine Instanz einer Klasse ist ein Objekt. Analog kann man sich das für Standard-C so vorstellen: man definiert eine Struktur und deklariert dann Variablen mit Hilfe dieser Struktur. Wenn ich "struct Screen scr;" schreibe, ist scr eine Instanz der Klasse Screen. scr ist ein Objekt.
Das Ableiten einer Klasse "Auto" bedeutet, einer neuen Klasse "Opel" neben ihren eigenen Features auch die Funktionen / Datenstruktur der Klasse "Auto" zu geben.
"Opel" hat sozusagen einen Teil seiner Funktionalität von "Auto" geerbt. "Opel" ist eine Subklasse von "Auto", "Auto" ist die Superklasse von "Opel". (Ein Objekt kann auch mehrere Superklassen haben, aber soweit ich weiß gibt es sowas in MUI bzw. BOOPSI nicht.)
3. Wie sieht eine Klasse aus, was wird von ihr erwartet?
3.1 Subklassen von Gadgets
3.1.1 Subklasse erzeugen, MUI_CreateCustomClass()
Eine Klasse läßt sich eigentlich erschreckend einfach erzeugen: MUI_CreateCustomClass().
Diese MUI-Funktion liefert bei Erfolg einen Zeiger (MUI_CustomClass *) auf die erzeugte Klasse. Damit man auch ein Objekt der Klasse erzeugen kann, sonst wäre ja alles umsonst gewesen, muß man NewObject() aufrufen.
NewObject() ist eine Funktion von Intuition und erwartet einen Zeiger auf eine IClass. Dieser ist in der erhaltenen MUI_CustomClass-Strukur enthalten. Weiter unten folgt dieser Ablauf nochmal als Programmtext.
MUI_CreateCustomClass() erwartet natürlich einige Argumente. Anhand einer selbstgeschriebenen Klasse will ich nun den Zusammenhang verdeutlichen. Die Klasse ist von MUIC_Slider abgeleitet, die Objekte sind Slider, die ihren Wert aber im "Minuten:Sekunden"-Format darstellen. Die Klasse heißt TimeClass (was vielleicht unglücklich gewählt ist, "TimeSliderClass" wäre sicher besser).
Wo ich gerade bei MUI_CreateCustomClass() war: der Aufruf sieht hier so aus:
// als globale Variable struct MUI_CustomClass *CL_Time; // z.B. in main(), jedenfalls bevor die MUI-Applikation aufgebaut // wird und nachdem die muimaster.library geöffnet wurde, natürlich CL_Time = MUI_CreateCustomClass(NULL,MUIC_Slider,NULL, sizeof(struct TimeData),TimeDispatcher);
MUIC_Slider ist ein #define für "Slider.mui", hier wird die sog. Superclass festgelegt, also die Klasse, von der unsere TimeClass abgeleitet wird.
Die TimeClass erbt damit die Eigenschaften und Möglichkeiten von Slider.mui, also werden wir in der eigenen Klasse keinen Slider zeichnen müssen, nur weil die Angabe des Werts nach unseren Wünschen geschehen soll.
Schon wieder eine NULL. Dieses Argument wird benötigt, wenn die Superklasse keine eingebaute MUI-Klasse (wie Slider.mui), sondern selbst eine Custom Class ist. (Sozusagen für den Fall, daß wir TimeClass ableiten wollten.)
sizeof(struct TimeData): langsam geht's ans Eingemachte.
Ein Objekt hat es evtl. nötig, Daten für die Dauer seiner Existenz zwischenzuspeichern. Der Speicherplatz, der von unserem Klassen-Code bearbeitet wird, muß von MUI für jedes Objekt der Klasse neu angelegt werden, damit sich die Objekte nicht in die Quere kommen.
Man denke an die Editor-Klasse. In jedem Editor-Fenster (in den Editor-Objekten) soll ja ein eigener Text zu editieren sein. Hier wird die Größe der Daten übergeben, über die jede TimeClass verfügen muß, praktischerweise macht man sich eine Struktur dafür. Der Zweck und Inhalt der Struktur ist dem Programmierer überlassen. Wie die "struct TimeData" im Beispiel aussieht, folgt bei Gelegenheit.
TimeDispatcher: das Herzstück der Klasse.
3.1.2 Der Dispatcher
TimeDispatcher ist der Name einer Funktion im Quelltext, die von MUI zu verschiedenen Anlässen aufgerufen wird. Was hier übergeben wird, ist ein Zeiger auf die Funktion.
Im Beipiel sieht die Funktion so aus:
ULONG TimeDispatcher(REG(a0) struct IClass *cl,REG(a2) Object *obj,REG(a1) Msg msg) { if (msg->MethodID==MUIM_Numeric_Stringify) { struct TimeData *data = (TimeData *) INST_DATA(cl,obj); struct MUIP_Numeric_Stringify *m = (MUIP_Numeric_Stringify *) msg; sprintf( data->buf, "%ld:%02ld", m->value / 60, m->value % 60 ); return((ULONG)data->buf); } return(DoSuperMethodA(cl,obj,msg)); }
Aber der Reihe nach:
ULONG TimeDispatcher(REG(a0) struct IClass *cl,REG(a2) Object *obj,REG(a1) Msg msg)
Der Funktionskopf ist standardisiert. MUI übergibt verschiedene Daten an den Dispatcher, sie sind in vordefinierten Prozessorregistern vorhanden.
Nun ist C ja eine Hochsprache, aber dem Compiler kann man vorschreiben, daß er die Parameter aus Registern holen soll. Das passiert hier mit REG().
REG() ist bloß ein Makro, für MaxonC++ ist es wie folgt definiert:
#define REG(x) register __ ## x
Aus "REG(a0)" wird also "register __a0". Übergeben wird ein Zeiger auf die Klasse des Objekts, ein Zeiger auf das Objekt selbst, sowie ein Zeiger auf eine "BOOPSI"-Message. (Hier steht kein *, Msg ist aber per typedef schon Zeiger, siehe intuition/classusr.h)
Diese Message beginnt immer mit einer MethodID, einer Zahl die festlegt, welche Methode der Klasse aufgerufen werden soll. Die Aufgabe des Dispatchers ist es, zu entscheiden, ob diese Methode von der Klasse selbst gestellt wird, dann ruft er eine eigene Funktion auf (im Beispiel hab ich mir die extra Funktion gekniffen), oder nicht. Dann gibt er die MethodID zusammen mit den erhaltenen Funktionen an die Superklasse weiter.
Bestimmte Methoden sind allgemeingültig definiert. Darüber hinaus kann eine Klasse eigene Methoden haben. Allgemein gibt es beispielsweise:
OM_NEW: Anlegen des Objekts (Konstruktor), OM_GET: Ein Attribut auslesen,
und einige weitere. Eine Methode der Sliderklasse ist MUIM_Numeric_Stringify, und diese will die TimeClass ändern.
(MUIM_Numeric_Stringify wandelt eine Zahl in einen darstellbaren String um. Der String wird im Slider bzw. daneben ausgegeben)
Also wird die Msg geprüft:
if (msg->MethodID==MUIM_Numeric_Stringify) {
wenn ja, macht der Dispatcher jetzt sein eigenes Ding, d.h. ein Slidertext nach eigenem Format kommt heraus. Im Folgenden wird dann ein Text erstellt, der dem "Minuten:Sekunden"-Format entspricht.
...return...; } return(DoSuperMethodA(cl,obj,msg));
Aber halt, eben habe ich unterstellt, daß MUIM_Numeric_Stringify eine Methode von Slider.mui ist. Wie kann das denn sein, da steht doch eindeutig "Numeric"? Ein Blick in das Include mui.h wirkt wieder Wunder:
+--Numeric (base class for slider gadgets)Slider ist also von Numeric abgeleitet, hat die Eigenschaft, eine Zahl in einen String zu wandeln, selbst von Numeric geerbt. Das ist auch gut so, schließlich sind Slider nicht die einzige Möglichkeit, um Zahlwerte einzugeben.
Leider wird aus dem Hierarchiebaum in mui.h nicht sofort ersichtlich, daß Slider von Numeric abgeleitet ist.
Nun habe ich die grobe Struktur der Custom Class im Beispiel erklärt. Eine Custom Class erhält Aufrufe von Methoden und kann sie selbst bearbeiten (muß sie auch, wenn es sich um neue, eigene Methoden handelt) oder gibt sie an ihre Superklasse weiter.
Was die eigene Methode, die MUIM_Numeric_Stringify überdecken soll,
nun eigentlich macht, folgt jetzt:
struct TimeData *data = (TimeData *) INST_DATA(cl,obj);
Hier erhält das Objekt einen Zeiger auf seine Daten. Diese struct
TimeData sieht folgendermaßen aus:
struct TimeData { char buf[32]; };
struct MUIP_Numeric_Stringify *m = (MUIP_Numeric_Stringify *) msg;
struct MUIP_Numeric_Stringify { ULONG MethodID; LONG value; };
Mit der MethodID fangen alle MUIP_#?-Strukturen an, die für die Parameterübergabe nötig sind (daher das `P'). Danach folgen beliebig viele Parameter, die beim DoMethod-Aufruf zu übergeben sind.
Bei MUIM_Numeric_Stringify. das ja den Zahlenwert eines numerischen Gadgets in einen Ausgabestring umwandelt, wird logischerweise der Zahlenwert erwartet. (Wenn man eigene MUIP_-Strukturen für eigene Methoden entwirft, sollte man aber nur Datenfelder in der Struktur haben, die vier Bytes groß sind [ULONG x, LONG x, APTR x, und alle denkbaren Pointer auf irgendwas, z.B. char *x]. Ich habe diese Information zwar nirgendwo gefunden, aber es erscheint logisch.)
sprintf( data->buf, "%ld:%02ld", m->value / 60, m->value % 60 );
Wichtig ist noch, daß hier das Datenfeld des Objekts angesprochen wird. Allerdings ist der Zweck hier nur, beim
return((ULONG)data->buf);
einen Zeiger zu erhalten, der auch nach dem Ende der Dispatcher-Funktion noch gültig ist. Eine globale Variable (oder eine lokale, die als "static" deklariert wurde) hätte für diesen Zweck auch genügt, aber wenn man die TimeClass als öffentliche Klasse (als .mcc) implementiert wäre, stünde sie in einer Library, und dann kann man nicht ausschließen, das es zu Konflikten kommen könnte. Generell ist man auf der sicheren Seite, wenn man das Objekt nur an seine eigene Datenstruktur heranläßt, da diese für jedes Objekt getrennt vorliegt.
Nochmal die TimeClass als zusammenhängender Quelltext:struct MUI_CustomClass *CL_Time; struct TimeData { char buf[32]; }; ULONG TimeDispatcher(REG(a0) struct IClass *cl,REG(a2) Object *obj,REG(a1) Msg msg) { if (msg->MethodID==MUIM_Numeric_Stringify) { struct TimeData *data = (TimeData *) INST_DATA(cl,obj); struct MUIP_Numeric_Stringify *m = (MUIP_Numeric_Stringify *) msg; sprintf( data->buf, "%ld:%02ld", m->value / 60, m->value % 60 ); return((ULONG)data->buf); } return(DoSuperMethodA(cl,obj,msg)); } void InitTimeClass() { CL_Time = MUI_CreateCustomClass(NULL,MUIC_Slider,NULL,sizeof(struct TimeData),TimeDispatcher); }
Dazu ruft man nach dem Öffnen der muimaster.library und vor dem Erstellen des Application-Objekts, das ja bereits die TimeClass irgendwo als Child enthalten soll, die oben gezeigte Funktion InitTimeClass() auf.
CL_Time muß auf NULL getestet werden, dann hat das Programm abzubrechen.
Jetzt folgt der Aufbau des App-Objekts. Irgendwo darin:
Child, MS_Time = NewObject(CL_Time->mcc_Class,0, MUIA_Numeric_Min , 10, MUIA_Numeric_Max , 600, MUIA_Numeric_Value, 100, MUIA_ControlChar , 't', TAG_DONE),
Das MS_Time läßt sich jetzt wie jedes andere Slider-Objekt benutzen, Werte setzen und auslesen geht also ganz normal. Natürlich kann man sich noch eigene MethodIDs mit eigener MUIP_Time_xy-Struktur ausdenken, die die Klasse unterstützt. In der Dispatcher-Funktion werden sie dann abgefragt, und die TimeClass reagiert entsprechend (speichert z.B. den Wert in eine übergebene Preferences-Datei o.ä...).
Wer das NewObject aus dem Quelltext verbannen möchte, kann natürlich ein #define anlegen, wie es MUI auch für seine Objekte macht. "TAG_DONE)" wird immer durch "End" ersetzt.
Jau, das war's schon. Mit der vorgestellten Methode ist schon eine Menge zu erreichen, z.B. auch Gadgets, die gleichzeitig eine feine Einstellung für kleine Zahlen und eine grobe Einstellung für große Zahlen ermöglichen.
Eigene Fensterklassen und bestimmte Finessen möchte ich im nächsten Kursteil besprechen, da jetzt die Grundlagen dazu aufgebaut sind. Wer Lust hat, kann sich ja jetzt schon mal den psi.c ansehen, aus dem MUI-Developerarchiv. Obwohl sich der folgende Kursteil nicht zu stark am PSI orientieren wird.
Wie man wirklich Gadgets selbst malt, kommt dann im nächsten oder (je nach Umfang, mal sehen) im übernächsten Kursteil.![]() |
Inhaltsverzeichnis | ![]() |
©`98Der AmZeiger |